﻿// Copyright 2008 (c) Michael Reichenauer, SWEDEN. All rights reserved.
//---------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using PostSharp.Laos;
using PSMock.Implementation;

namespace PSMock
{

	/// <summary>
	/// The main class for providing mocking functionality.
	/// </summary>
	public static class Mock
	{
		// tracks if running in mocking and setup scope
		private static bool mockingScope = false;
		private static bool setupScope = false;

		private static MockedFunctions mockedFunctions = new MockedFunctions();

		// While in setup scope, the current (last called) function
		private static MockedFunction currentFunction = null;


		/// <summary>
		/// Mocking property is used in a using statement to start a Mocking scope
		/// </summary>
		public static IDisposable Mocking
		{
			get
			{
				if (mockingScope) throw new InvalidOperationException(
					"Nested mocking scopes are not supported.");
				return new MockingScope();
			}
		}

		/// <summary>
		/// Setup property is used in a using statement to start Setup scope
		/// </summary>
		public static IDisposable Setup
		{
			get
			{
				if (!mockingScope || setupScope) throw new InvalidOperationException(
					"Setup only supported within a Mocking scope.");
				return new SetupScope();
			}
		}


		/// <summary>
		/// Mock all members in all instances for the specified type. Get properties and functions 
		/// will return default values, while set properties and methods calls will be ignored.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		public static void AllMembersInType<T>()
		{
			if (!setupScope) throw CreateNotWithinSetupScopeException();
			mockedFunctions.AddType(typeof(T));
		}


		/// <summary>
		/// Mock all members in the specified instance. Get properties and functions 
		/// will return default values, while set properties and methods calls will be ignored.
		/// </summary>
		/// <param name="instance">The instance.</param>
		public static void AllMembersInInstance(object instance)
		{
			if (instance == null) throw new ArgumentNullException("instance");
			if (!setupScope) throw CreateNotWithinSetupScopeException();

			mockedFunctions.AddInstance(instance);
		}


		/// <summary>
		/// Mock the function called as the parameter. E.g. 
		/// <code>
		/// Mock.Function(mocked.Echo(null))
		///   .Return("mock");
		/// </code>
		/// </summary>
		public static FunctionOption Function(object functionCallReturnValue)
		{
			if (!setupScope) throw CreateNotWithinSetupScopeException();

			if (currentFunction == null) throw new InvalidOperationException(
				"No function called, or PostSharp processing is disabled");

			FunctionOption option = new FunctionOption(currentFunction);
			currentFunction = null;

			return option;
		}



	
		/// <summary>
		/// Mock the method called in the delegate parameter. E.g. 
		/// <code>
		/// Mock.Method(() => { mocked.Set(null); })
		///	  .Throw(new ArgumentException("argument error"));
		///	</code>
		/// </summary>
		public static MethodOption Method(Action method)
		{
			if (!setupScope) throw CreateNotWithinSetupScopeException();

			method.Invoke();

			if (currentFunction == null) throw new InvalidOperationException(
				"No function called, or PostSharp processing is disabled");

			MethodOption option = new MethodOption(currentFunction);
			currentFunction = null;
			return option;
		}




		#region Function generics

		/// <summary>
		/// Mock the specified function.
		/// </summary>
		public static FunctionOption<TResult> Function<TResult>(Func<TResult> function)
		{
			return new FunctionOption<TResult>(AddMockedFunction(function));
		}
		public static FunctionMock<T1, TResult> Function<T1, TResult>(Func<T1, TResult> function)
		{
			return new FunctionMock<T1, TResult>(AddMockedFunction(function));
		}
		public static FunctionMock<T1, T2, TResult> Function<T1, T2, TResult>(Func<T1, T2, TResult> function)
		{
			return new FunctionMock<T1, T2, TResult>(AddMockedFunction(function));
		}
		public static FunctionMock<T1, T2, T3, TResult> Function<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> function)
		{
			return new FunctionMock<T1, T2, T3, TResult>(AddMockedFunction(function));
		}
		public static FunctionMock<T1, T2, T3, T4, TResult> Function<T1, T2, T3, T4, TResult>(Func<T1, T2, T3, T4, TResult> function)
		{
			return new FunctionMock<T1, T2, T3, T4, TResult>(AddMockedFunction(function));
		}

		#endregion


		#region Method generics

		/// <summary>
		/// Mock the specified method.
		/// </summary>
		public static MethodMock<T1> Method<T1>(Action<T1> method)
		{
			return new MethodMock<T1>(AddMockedFunction(method));
		}
		public static MethodMock<T1, T2> Method<T1, T2>(Action<T1, T2> method)
		{
			return new MethodMock<T1, T2>(AddMockedFunction(method));
		}
		public static MethodMock<T1, T2, T3> Method<T1, T2, T3>(Action<T1, T2, T3> method)
		{
			return new MethodMock<T1, T2, T3>(AddMockedFunction(method));
		}
		public static MethodMock<T1, T2, T3, T4> Method<T1, T2, T3, T4>(Action<T1, T2, T3, T4> method)
		{
			return new MethodMock<T1, T2, T3, T4>(AddMockedFunction(method));
		}

		#endregion



		private static MockedFunction AddMockedFunction(Delegate method)
		{
			if (!setupScope) throw CreateNotWithinSetupScopeException();

			return AddMockedFunction(method.Method.MetadataToken, method.Target);
		}


		private static MockedFunction AddMockedFunction(int methoddataToken, object instance)
		{
			if (!setupScope) throw CreateNotWithinSetupScopeException();

			MockedFunction mockedFunction = new MockedFunction();
			mockedFunction.MetadataToken = methoddataToken;

			mockedFunction.Instance = instance;

			mockedFunctions.AddFunction(mockedFunction);

			return mockedFunction;
		}




		/// <summary>
		/// Called for each method during the mocking phase
		/// </summary>
		/// <param name="eventArgs">The <see cref="PostSharp.Laos.MethodExecutionEventArgs"/> 
		/// instance containing the event data.</param>
		internal static void Intercept(MethodInvocationEventArgs eventArgs, int methoddataToken)
		{
			if (mockingScope)
			{
				if (setupScope)
				{
					InterceptDuringSetup(eventArgs, methoddataToken);
				}
				else
				{
					InterceptDuringMocking(eventArgs, methoddataToken);
				}
			}
			else
			{
				eventArgs.Proceed();
			}
		}



		/// <summary>
		/// Intercepted during setup phase.
		/// </summary>
		/// <param name="eventArgs">The <see cref="PostSharp.Laos.MethodExecutionEventArgs"/> instance containing the event data.</param>
		private static void InterceptDuringSetup(MethodInvocationEventArgs eventArgs, int methoddataToken)
		{
			if (eventArgs.Delegate.Method.IsConstructor)
			{
				// Constructor called during mocking phase, ignored
			}
			else
			{
				currentFunction = AddMockedFunction(methoddataToken, eventArgs.Delegate.Target);

				eventArgs.ReturnValue = Util.DefaultValue(eventArgs.Delegate.Method);
			}
			//eventArgs.FlowBehavior = FlowBehavior.Return;
		}



		/// <summary>
		/// Intercepted during mocking phase.
		/// </summary>
		/// <param name="eventArgs">The <see cref="PostSharp.Laos.MethodExecutionEventArgs"/> instance containing the event data.</param>
		private static void InterceptDuringMocking(MethodInvocationEventArgs eventArgs, int methoddataToken)
		{
			MockedFunction mockedFunction = mockedFunctions.FindMockedMethod(eventArgs, methoddataToken);

			if (mockedFunction == null)
			{
				// function that was not setup to be mocked, continue the call to the original function
				eventArgs.Proceed();
			}
			else
			{
				// The call should be mocked

				if (mockedFunction.ThrowException != null)
				{
					// A mocked exception was specified
					throw mockedFunction.ThrowException;
				}
				else if (mockedFunction.Function != null)
				{
					// Call the mock function to return value
					try
					{
						eventArgs.ReturnValue = mockedFunction.Function.DynamicInvoke(eventArgs.GetArgumentArray());
					}
					catch (Exception e)
					{
						// Exceptions thrown with a delegate body will be wrapped inside a TargetInvocationException
						// so we need to unwrap the original exception
						if (e is TargetInvocationException)
							throw e.InnerException;
						else
							throw;
					}
				}
				else
				{
					// Return the mocked value
					eventArgs.ReturnValue = mockedFunction.ReturnValue;
				}

				// Ensure that original function is not called
				//eventArgs.FlowBehavior = FlowBehavior.Return;
			}
		}




		/// <summary>
		/// Creates the "Not within setup scope exception".
		/// </summary>
		private static InvalidOperationException CreateNotWithinSetupScopeException()
		{
			return new InvalidOperationException("Only supported within a Setup scope.");
		}


		/// <summary>
		/// Clears all mocking information at the end of the mocking scope.
		/// </summary>
		private static void ResetMockings()
		{
			mockingScope = false;
			setupScope = false;
			currentFunction = null;
			mockedFunctions = new MockedFunctions();
		}


		/// <summary>
		/// Class to support the using statements for wrapping a mocking scope
		/// </summary>
		private class SetupScope : IDisposable
		{
			public SetupScope()
			{
				setupScope = true;
			}
			public void Dispose()
			{
				setupScope = false;
			}
		}


		/// <summary>
		/// Class to support the using statements for setup phase scope within a mocking scope
		/// </summary>
		private class MockingScope : IDisposable
		{
			public MockingScope()
			{
				mockingScope = true;
			}
			public void Dispose()
			{
				ResetMockings();
			}
		}
	}
}
